﻿using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;

namespace Hive.Core.Common.Shell
{
    /// <summary>
    /// A shell implementation based on the <see cref="Console"/>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This implementation uses different colors to distinguish messages of different
    /// types and severity.
    /// </para>
    /// </remarks>
    public class ConsoleShell : IShell, IDisposable
    {
        private readonly object syncRoot = new object();
        private readonly ConsoleShellOptions options;
        private readonly bool originalTreatControlCAsInput;
        private EventHandler canceled;
        private bool isDisposed;
        private bool isCanceled;
        private int cancelKeyPressRepeatCount;
        private Stopwatch cancelKeyPressRepeatIntervalStopwatch;

        /// <summary>
        /// Creates a console shell object.
        /// </summary>
        /// <param name="options">The console shell options.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="options"/> is null.</exception>
        public ConsoleShell(ConsoleShellOptions options)
        {
            if (options == null)
                throw new ArgumentNullException("options");

            this.options = options;

            originalTreatControlCAsInput = Console.TreatControlCAsInput;
            Console.TreatControlCAsInput = false;
            Console.CancelKeyPress += HandleCancelKeyPress;
        }

        /// <summary>
        /// Restores the <see cref="Console" /> settings that existed before the console shell was created.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Restores the <see cref="Console" /> settings that existed before the console shell was created.
        /// </summary>
        /// <param name="disposing">True if <see cref="Dispose()"/> was called directly.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                lock (syncRoot)
                {
                    if (! isDisposed)
                    {
                        isDisposed = true;
                        Console.CancelKeyPress -= HandleCancelKeyPress;
                        Console.TreatControlCAsInput = originalTreatControlCAsInput;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the synchronization root for guarding multithreaded access.
        /// </summary>
        protected object SyncRoot
        {
            get { return syncRoot; }
        }

        /// <summary>
        /// Gets the console shell options.
        /// </summary>
        public ConsoleShellOptions Options
        {
            get
            {
                lock (syncRoot)
                {
                    ThrowIfDisposed();
                    return options;
                }
            }
        }

        /// <inheritdoc />
        public bool IsCanceled
        {
            get
            {
                lock (syncRoot)
                {
                    ThrowIfDisposed();
                    return isCanceled;
                }
            }
        }

        /// <inheritdoc />
        public void Cancel()
        {
            EventHandler eventHandler;
            lock (syncRoot)
            {
                ThrowIfDisposed();

                if (isCanceled)
                    return;

                isCanceled = true;
                eventHandler = canceled;

                Echo(MessageSeverity.Warning, "Canceling!");
            }

            if (eventHandler != null)
                eventHandler(this, EventArgs.Empty);
        }

        /// <inheritdoc />
        public event EventHandler Canceled
        {
            add
            {
                lock (syncRoot)
                {
                    ThrowIfDisposed();
                    canceled += value;

                    if (!isCanceled)
                        return;
                }

                if (value != null)
                    ThreadPool.QueueUserWorkItem(dummy => value(this, EventArgs.Empty));
            }
            remove
            {
                lock (syncRoot)
                {
                    ThrowIfDisposed();
                    canceled -= value;
                }
            }
        }

        /// <inheritdoc />
        public DirectoryInfo CurrentDirectory
        {
            get { return new DirectoryInfo(Environment.CurrentDirectory); }
        }

        /// <inheritdoc />
        public Verbosity Verbosity
        {
            get { return options.Verbosity; }
            set { options.Verbosity = value; }
        }

        /// <inheritdoc />
        public string Prompt(string prompt)
        {
            if (prompt == null)
                throw new ArgumentNullException("prompt");

            lock (syncRoot)
            {
                ThrowIfDisposed();

                WriteToStandardOutput(options.PromptColor, "", true);
                WriteToStandardOutput(options.PromptColor, prompt, true);
                WriteToStandardOutput(options.PromptColor, "> ", false);

                var line = new StringBuilder();

                bool promptCanceled;
                for (; ; )
                {
                    promptCanceled = IsCanceled;
                    if (promptCanceled)
                        break;

                    int ch = Console.Read();
                    promptCanceled = IsCanceled || ch == -1 /*end of stream*/ || ch == 3 /*ctrl-C*/;
                    if (promptCanceled)
                        break;

                    if (ch == 13 /*carriage return*/)
                        break;

                    if (!char.IsControl((char)ch))
                        line.Append((char)ch);
                }

                if (promptCanceled)
                {
                    WriteToStandardOutput(options.PromptColor, "", true);
                    throw new OperationCanceledException("Prompt was canceled.");
                }

                WriteToStandardOutput(options.PromptColor, "", true);
                return line.ToString().Trim(); ;
            }
        }

        /// <inheritdoc />
        public void Echo(MessageSeverity severity, string message)
        {
            if (message == null)
                throw new ArgumentNullException("message");

            lock (syncRoot)
            {
                ThrowIfDisposed();

                switch (severity)
                {
                    case MessageSeverity.Error:
                        WriteToStandardError(options.ErrorMessageColor, message, true);
                        break;

                    case MessageSeverity.Warning:
                        WriteToStandardError(options.WarningMessageColor, message, true);
                        break;

                    case MessageSeverity.Important:
                        if (options.Verbosity >= Verbosity.Normal)
                            WriteToStandardOutput(options.ImportantMessageColor, message, true);
                        break;

                    case MessageSeverity.Info:
                        if (options.Verbosity >= Verbosity.Detailed)
                            WriteToStandardOutput(options.InfoMessageColor, message, true);
                        break;

                    case MessageSeverity.Debug:
                        if (options.Verbosity == Verbosity.Detailed)
                            WriteToStandardOutput(options.DebugMessageColor, message, true);
                        break;

                    default:
                        throw new ArgumentOutOfRangeException("severity");
                }
            }
        }

        /// <inheritdoc />
        public void EchoException(string message, Exception exception)
        {
            if (message == null)
                throw new ArgumentNullException("message");
            if (exception == null)
                throw new ArgumentNullException("exception");

            WriteToStandardError(options.ErrorMessageColor, message, true);

            if (options.Verbosity >= Verbosity.Detailed)
            {
                WriteToStandardError(options.ErrorMessageColor, exception.ToString(), true);
            }
            else
            {
                do
                {
                    WriteToStandardError(options.ErrorMessageColor, exception.Message, true);
                    exception = exception.InnerException;
                }
                while (exception != null);
            }
        }

        /// <inheritdoc />
        public int Execute(ShellCommand shellCommand, StandardOutputConsumer standardOutputConsumer, StandardErrorConsumer standardErrorConsumer)
        {
            if (shellCommand == null)
                throw new ArgumentNullException("shellCommand");

            lock (syncRoot)
            {
                ThrowIfDisposed();

                if (isCanceled && shellCommand.Cancelable)
                    ThrowShellCommandCanceledException(shellCommand);
            }

            var processStartInfo = new ProcessStartInfo
            {
                FileName = shellCommand.FilePath,
                Arguments = shellCommand.Arguments,
                WorkingDirectory = shellCommand.WorkingDirectory.FullName,
                CreateNoWindow = true,
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                StandardOutputEncoding = Console.OutputEncoding,
                StandardErrorEncoding = Console.OutputEncoding
            };

            var process = new Process()
            {
                StartInfo = processStartInfo,
                EnableRaisingEvents = true
            };

            var outputDataFinished = new ManualResetEvent(false);
            process.OutputDataReceived += (sender, e) =>
            {
                if (e.Data == null)
                {
                    outputDataFinished.Set();
                }
                else if (standardOutputConsumer == null || standardOutputConsumer(e.Data))
                {
                    if (options.Verbosity >= Verbosity.Normal)
                        WriteToStandardOutput(options.StandardOutputColor, e.Data, true);
                }
            };

            var errorDataFinished = new ManualResetEvent(false);
            process.ErrorDataReceived += (sender, e) =>
            {
                if (e.Data == null)
                {
                    errorDataFinished.Set();
                }
                else if (standardErrorConsumer == null || standardErrorConsumer(e.Data))
                {
                    WriteToStandardOutput(options.StandardErrorColor, e.Data, true);
                }
            };

            Echo(MessageSeverity.Debug, string.Format("Executing shell command: {0}", shellCommand));

            try
            {
                process.Start();
            }
            catch (Exception ex)
            {
                throw new ShellExecuteException(string.Format("Could not execute shell command '\"{0}\"'.", shellCommand), ex);
            }

            EventHandler canceledEventHandler = null;
            bool killed = false;
            if (shellCommand.Cancelable)
            {
                canceledEventHandler = (sender, e) =>
                {
                    try
                    {
                        if (!process.HasExited)
                        {
                            Echo(MessageSeverity.Debug, "Killing shell command due to cancelation.");
                            process.Kill();
                            killed = true;
                        }
                    }
                    catch
                    {
                        // Ignore any problems since it is possible that the process handle
                        // has become invalid so it cannot be killed.
                    }
                };
            }

            try
            {
                Canceled += canceledEventHandler;

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();
                process.WaitForExit();
            }
            finally
            {
                Canceled -= canceledEventHandler;
            }

            outputDataFinished.WaitOne();
            errorDataFinished.WaitOne();

            Echo(MessageSeverity.Debug, string.Format("Shell command exited with code {0}.", process.ExitCode));

            if (killed)
                ThrowShellCommandCanceledException(shellCommand);

            return process.ExitCode;
        }

        private static void ThrowShellCommandCanceledException(ShellCommand shellCommand)
        {
            throw new OperationCanceledException(string.Format("Canceled shell command: {0}", shellCommand));
        }

        /// <summary>
        /// Writes to standard output.
        /// </summary>
        /// <param name="color">The color of the line.</param>
        /// <param name="message">The message, not null.</param>
        /// <param name="newLine">True to add a newline after writing the message.</param>
        protected virtual void WriteToStandardOutput(ConsoleColor color, string message, bool newLine)
        {
            WriteLineToConsole(color, message, Console.Out, newLine);
        }

        /// <summary>
        /// Writes to standard error.
        /// </summary>
        /// <param name="color">The color of the line.</param>
        /// <param name="message">The message, not null.</param>
        /// <param name="newLine">True to add a newline after writing the message.</param>
        protected virtual void WriteToStandardError(ConsoleColor color, string message, bool newLine)
        {
            WriteLineToConsole(color, message, Console.Error, newLine);
        }

        private void WriteLineToConsole(ConsoleColor color, string message, TextWriter writer, bool newLine)
        {
            lock (syncRoot)
            {
                ConsoleColor originalForegroundColor = Console.ForegroundColor;

                Console.ForegroundColor = color;

                if (newLine)
                    writer.WriteLine(message);
                else
                    writer.Write(message);

                Console.ForegroundColor = originalForegroundColor;
            }
        }

        private void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e)
        {
            if (isDisposed)
                return;

            lock (syncRoot)
            {
                if (cancelKeyPressRepeatCount == 0
                    || cancelKeyPressRepeatIntervalStopwatch.Elapsed > options.ForceQuitCancelKeyPressTimeout)
                {
                    cancelKeyPressRepeatCount = 0;
                    cancelKeyPressRepeatIntervalStopwatch = Stopwatch.StartNew();
                }

                cancelKeyPressRepeatCount += 1;
                if (cancelKeyPressRepeatCount >= options.ForceQuitCancelKeyPressRepetitions)
                    e.Cancel = true;
            }

            Cancel();
        }

        private void ThrowIfDisposed()
        {
            if (isDisposed)
                throw new ObjectDisposedException("ConsoleShell");
        }
    }
}
